实现注册、登录、JWT 鉴权、密码加密
安装插件
# jwt 加密
pnpm install passport passport-jwt passport-local @nestjs/jwt @nestjs/passport -S
pnpm install @types/passport-local -D
# 加密/解密
pnpm install argon2 -S创建 auth 模块、控制器、提供器
- 创建 auth 模块
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginStrategy } from './login.strategy';
import { JwtStrategy } from './jwt.strategy';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { AuthController } from './auth.controller';
import { PasswordService } from './password.service';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '1d' },
}),
],
controllers: [AuthController],
providers: [AuthService, LoginStrategy, JwtStrategy, PasswordService],
exports: [AuthService],
})
export class AuthModule {}- 创建 auth 控制器
// src/auth/auth.controller.ts
import { Controller, Request, Post, UseGuards, Body, Get } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginAuthGuard } from './login-auth.guard';
import { Public } from '../meta';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// 登录
@UseGuards(LoginAuthGuard)
@Public()
@Post('/login')
async login(@Request() req: any) {
return this.authService.login(req.user);
}
// 注册
@Public() // 不加 jwt 拦截
@Post('/register')
async register(@Body() userDto: any) {
const { username, password } = userDto;
return this.authService.register(username, password);
}
@Public() // 不加 jwt 拦截
@Get('/getUsers')
async findAll() {
return await this.authService.findAll();
}
// 添加 jwt 拦截(从 jwt 中解析当前用户信息)
@Get('/getUser')
async findOne(@Request() req: any) {
return await this.authService.findOne(req.username);
// 如果是添加了 jwt 拦截的路由,则可以从 jwt 中解析用户信息,结果是jwt.strategy.ts中 validate 方法的返回值
// req.username: '赵毅'
// req.userId: 1
// 其他 http 请求方式参数,也可以通过这一方式获取,当然也可以从 @Param() @Body @Query 等装饰器中获取http 对应方法的参数
// req.params - 路由参数
// req.query - 查询参数
// req.body - 请求体
// req.headers - 请求头
// req.method - HTTP 方法
// req.url - 请求 URL
}
}- 创建 auth 服务
// src/auth/auth.service.ts
import { Injectable, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import * as argon2 from 'argon2';
import { PasswordService } from './password.service';
import { UserService } from '../user/user.service';
import { JwtService } from '@nestjs/jwt';
import { User } from '../user/user.entity';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private readonly passwordService: PasswordService
) {}
// 登录时的用户验证
async validateUser(username: string, pass: string): Promise<any> {
const user = await this.userService.findOne(username);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
const isValid = await this.passwordService.verifyPassword(user.password, pass);
if (!isValid) {
throw new UnauthorizedException('密码错误');
}
const { password, ...result } = user;
return result;
}
// 登录
async login(user: any) {
const payload = { username: user.username, sub: user.id };
return {
username: payload.username,
access_token: `Bearer ${this.jwtService.sign(payload)}`,
};
}
// 注册 & 密码加密argon2
async register(uname: string, pwd: string): Promise<any> {
const user = await this.userService.findOne(uname);
if (user) {
throw new ForbiddenException('此用户已经存在');
}
// argon2 加密
const hashedPassword = await this.passwordService.hashPassword(pwd);
const result = await this.userService.create({
username: uname,
password: hashedPassword,
});
if (result && result.username) {
return {
message: '注册成功',
data: {
id: result.id,
username: result.username,
},
};
}
}
async findAll(): Promise<User[]> {
return await this.userService.findAll();
}
async findOne(username: string): Promise<User> {
return await this.userService.findOne(username);
}
}创建登录守卫和策略
login-auth.guard.ts(login 路由守卫):此守卫触发后,会通过 passport 插件关联触发 login.strategy.ts 策略逻辑
login.strategy.ts(login passport 策略):在 login 路由守卫执行后触发此策略逻辑,只是借用 passport 策略方式,没做其他内部处理
执行自定义的查询 mysql 数据库逻辑,并完成登录信息校验,通过后返回用于 jwt 签名加密的用户信息
- 创建 Login 路由守卫
// src/auth/login-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LoginAuthGuard extends AuthGuard('local') {}- 创建 Login 路由策略
// src/auth/login.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LoginStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}创建JWT守卫和策略
jwt-auth.guard.ts(jwt 路由守卫):此守卫触发后,会通过 passport 插件关联触发 jwt.strategy.ts 策略逻辑 jwt.strategy.ts(jwt passport 策略):在 jwt 路由守卫执行后触发此策略逻辑,内部集成了获取和解析 token 的逻辑,通过后返回用户信息
- 创建JWT守卫
// src/auth/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../meta';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}- 创建JWT策略
// src/auth/login.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}- 定义 JWT 密钥
// src/auth/constants.ts
export const jwtConstants = {
secret: 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};全局 JWT 守卫(元信息)
- 将 JWT 守卫定义为全局守卫
由于绝大部分路由都需要进行 jwt 身份验证,我们无需在每一个路由上方加上 @UseGuards(JwtAuthGuard) 装饰器,而是,注册全局 jwt 验证路由守卫,只是排除不需要 jwt 验证的路由即可在 app.module.ts 中,把 jwt 路由守卫注册为全局路由守卫,此时所有路由默认都会被监视
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { User } from './user/user.entity';
import { Log } from './log/log.entity';
import { Roles } from './roles/roles.entity';
import { Profile } from './profile/profile.entity';
import { ProfileModule } from './profile/profile.module';
import { LogModule } from './log/log.module';
import { RolesModule } from './roles/roles.module';
import { AuthModule } from './auth/auth.module';
// 注册 jwt 全局路由守卫
const JwtGlobalGuard = {
provide: APP_GUARD,
useClass: JwtAuthGuard,
};
@Module({
imports: [
UserModule,
TypeOrmModule.forRoot({
type: 'mysql',
host: '47.92.68.193',
port: 3306,
username: 'root',
password: 'Wuweijie1.',
database: 'db_nest_orm',
entities: [User, Log, Roles, Profile],
synchronize: true, // 同步本地的schema到数据库(每次创建新的实体类保存后,在数据库中都会自动生成实体表)(生产环境设置 false)
logging: ['error'],
}),
ProfileModule,
LogModule,
RolesModule,
AuthModule,
],
controllers: [AppController],
providers: [JwtGlobalGuard, AppService],
})
export class AppModule {}- 创建排除元信息
路由元信息实现排除逻辑,新建 meta.ts 元信息,重构 jwt-auth.guard.ts 在 auth.controller.ts 中给排除的路由添加 @Public() 装饰器
// src/meta/index.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);实现注册&密码加密
argon2 方案:因为盐值随机变化因此可避免彩虹表攻击,因为没有密钥因此无法反向解密,只能通过 argon.verify(hashedPassword, plainPassword) 方法传入加密后和加密前的密码,
才可判断校验是否通过也就是说作为程序员的我们也无法逆向解析出用户的密码,如果用户需要修改密码,那么只能 update 修改用户的密码了
- 创建密码加密和解密服务
// src/auth/password.service.ts
import * as argon from 'argon2';
import { Injectable } from '@nestjs/common';
@Injectable()
export class PasswordService {
async hashPassword(password: string): Promise<string> {
return argon.hash(password);
}
async verifyPassword(
hashedPassword: string,
plainPassword: string,
): Promise<boolean> {
return argon.verify(hashedPassword, plainPassword);
}
}- 在 auth.service.ts 文件中,定义登录和注册服务,登录验证的时候解密,注册插入数据库的时候加密
创建用户的增删改查
// src/user/user.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user.entity';
import { FindOneOptions, Repository } from 'typeorm';
@Injectable()
export class UserService {
constructor(
@InjectRepository(User) private readonly userRepository: Repository<User>,
) {}
// 增
async create(user: Partial<User>): Promise<User> {
return await this.userRepository.save(user);
}
// 永久删除
async remove(id: number): Promise<any> {
return await this.userRepository.delete(id);
}
// 软删除
async softDeleteUser(id: number): Promise<any> {
return await this.userRepository.softDelete(id);
}
// 恢复软删除数据 http://localhost:3000/api/v1/user/softDelete/6
async restoreSoftDeleteUser(id: number): Promise<any> {
return await this.userRepository.restore(id);
}
// 查所有,包括软删除的记录
async softDeleteUserAllData(): Promise<User[]> {
return await this.userRepository.find({ withDeleted: true });
}
// 改
async update(user: Partial<User>): Promise<any> {
return await this.userRepository.update(user.id, user);
}
// 查所有,自动过滤掉已软删除的记录
async findAll(): Promise<User[]> {
return await this.userRepository.find();
}
// 查单条,自动过滤掉已软删除的记录
async findOne(username: string): Promise<User> {
const options: FindOneOptions<User> = {
where: { username },
};
return await this.userRepository.findOne(options);
}
}